第17章 事件

JavaScript 与 HTML 的交互是通过事件实现的,可以使用仅在事件发生时执行的监听器订阅事件。

IE 事件流被称为事件冒泡,事件被定义为从最具体的元素开始触 发,然后向上传播,例如如下 HTML 页面:

<!DOCTYPE html>
<html>
  <head>
    <title>Event Bubbling Example</title>
  </head>
  <body>
    <div id="myDiv">Click Me</div>
  </body>
</html>

在点击页面中的

元素后,click 事件会以如下顺序发生:
-> -> -> document

Netscape 团队提出了另一种名为事件捕获的事件流,最不具体的节点最先收到事件,而最具体的节点最后收到事件。如上页面中,click 事件会按照 document -> -> ->

的顺序传递。

DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。

以下介绍几种事件绑定方式:

// 方式1
<input type="button" value="Click Me" onclick="console.log('Clicked')" />
// 方式2
<script>
  function showMessage() {
    console.log("Hello world!");
  }
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>
// 方式3
let btn = document.getElementById("myBtn");
btn.onclick = function() {
  console.log("Clicked");
};
// 方式4
// 接收 3 个参数:事件名、事件处理函 数和一个布尔值
// true 表示在捕获阶段调用事件处理程序
// false (默认值)表示在冒泡阶段调用事件处理程序
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
  console.log(this.id);
}, false);
// 移除事件
btn.removeEventListener("click", function() {
  console.log(this.id);
}, false);
// IE8 及之前版本
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
  console.log("Clicked");
});
// 移除事件
btn.detachEvent("onclick", function() {
  console.log("Clicked");
})

DOM 事件对象

event 对象是传给事件处理程序的唯一参数,其包含与特定事件相关的属性和方法,不同的事件生成的事件对象也会包含不同 的属性和方法。下表记录了事件对象的公共属性和方法:

属性/方法 类型 说明
bubbles 布尔值 事件是否冒泡
cancelable 布尔值 是否可以取消事件的默认行为
currentTarget 元素 当前事件处理程序所在的元素
defaultPrevented 布尔值 true 表示已经调用 preventDefault() 方法
detail 整数 事件相关的其他信息
eventPhase 整数 调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段
preventDefault() 函数 取消事件的默认行为,cancelable 为 true 时可用
stopImmediatePropagation() 函数 取消所有后续事件捕获或事件冒泡
stopPropagation() 函数 取消所有后续事件捕获或事件冒泡,bubbles 为 true 时可用
target 元素 事件目标
trusted 布尔值 true 表示事件是由浏览器生成的。false 表示事件是开 发者通过 JS 创建的
type 字符串 被触发的事件类型
View AbstractView 与事件相关的抽象视图,等于事件所发生的 window 对象

事件类型

DOM3 Events 定义了如下事件类型:

  • 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件
  • 焦点事件(FocusEvent):在元素获得和失去焦点时触发
  • 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发
  • 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发
  • 输入事件(InputEvent):向文档中输入文本时触发
  • 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发
  • 合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发

用户界面事件

用户界面事件或 UI 事件不一定跟用户操作有关,主要有以下几种:

  • load:在 window 上当页面加载完成后触发;在窗套()上当所有窗格()都加载完成后触发;在元素上当图片加载完成后触发;在元素上当相应对象加 载完成后触发
  • unload:在 window 上当页面完全卸载后触发;在窗套上当所有窗格都卸载完成后触发;在元素上当相应对象卸载完成后触发
  • abort:在元素上当相应对象加载完成前被用户提前终止下载时触发
  • error:在 window 上当 JavaScript 报错时触发;在元素上当无法加载指定图片时触发;在元素上当无法加载相应对象时触发;在窗套上当一个或多个窗格无法完成加载时触发
  • select:在文本框(或 textarea)上当用户选择了一个或多个字符时触发
  • resize:在 window 或窗格上当窗口或窗格被缩放时触发
  • scroll:当用户滚动包含滚动条的元素时在元素上触发
  • load 事件

    window.addEventListener("load", (event) => {
      console.log("Loaded!");
    });
    <!DOCTYPE html>
    <html>
      <head>
        <title>Load Event Example</title>
      </head>
      <body onload="console.log('Loaded!')">
      </body>
    </html>

    unload 事件

    window.addEventListener("unload", (event) => {
      console.log("Unloaded!");
    });
    <!DOCTYPE html>
    <html>
      <head>
        <title>Load Event Example</title>
      </head>
      <body onunload="console.log('Unloaded!')">
      </body>
    </html>

    resize 事件

    window.addEventListener("resize", (event) => {
      console.log("Resized");
    });

    scroll 事件

    window.addEventListener("scroll", (event) => {
      if (document.compatMode == "CSS1Compat") {
        console.log(document.documentElement.scrollTop);
      } else {
        console.log(document.body.scrollTop);
      }
    });

    焦点事件

    焦点事件在页面元素获得或失去焦点时触发,焦点事件有以下几种:

    • focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。
    • focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版。
    • blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。
    • focusout:当元素失去焦点时触发。这个事件是 blur 的通用版。
    • DOMFocusIn:当元素获得焦点时触发。不推荐。
    • DOMFocusOut:当元素失去焦点时触发。不推荐。

    当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件:

    1. focuscout 在失去焦点的元素上触发。
    2. focusin 在获得焦点的元素上触发。
    3. blur 在失去焦点的元素上触发。
    4. DOMFocusOut 在失去焦点的元素上触发。
    5. focus 在获得焦点的元素上触发。
    6. DOMFocusIn 在获得焦点的元素上触发。

    鼠标和滚轮事件

    DOM3 定义了 9 种鼠标事件:

    • click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发
    • dblclick:在用户双击鼠标主键(通常是左键)时触发。
    • mousedown:在用户按下任意鼠标键时触发。
    • mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。
    • mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。
    • mousemove:在鼠标光标在元素上移动时反复触发。
    • mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。
    • mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。
    • mouseup:在用户释放鼠标键时触发。

    除了 mouseenter 和 mouseleave,所有的鼠标事件都会冒泡,都可以被取消。鼠标事件都是在浏览器视口中的某个位置上发生的。这些信息被保存在 event 对象的clientX 和 clientY 属性中。可以通过下面的方式获取鼠标事件的客户端坐标:

    let div = document.getElementById("myDiv");
    div.addEventListener("click", (event) => {
      console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`); 
    });

    页面坐标是事件发生时鼠标光标在页面上的坐标,通过 event 对象的 pageX 和 pageY 可以获取。这两个属性表示鼠标光标在页面上的位置,因此反映的是光标到页面而非视口左边与上边的距离。

    let div = document.getElementById("myDiv");
    div.addEventListener("click", (event) => {
      console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`); 
    });

    鼠标事件不仅是在浏览器窗口中发生的,也是在整个屏幕上发生的。可以通过 event 对象的 screenX 和 screenY 属性获取鼠标光标在屏幕上的坐标。

    let div = document.getElementById("myDiv");
    div.addEventListener("click", (event) => {
      console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`);
    });

    键盘上的修饰键 Shift、Ctrl、Alt 和 Meta 经常用于修改鼠标事件的行为。DOM 规定了 4 个属性来表示这几个修饰键的状态:shiftKey、ctrlKey、altKey 和 metaKey。这几个属性会在各自对应的修饰键被按下时包含布尔值 true,没有被按下时包含 false。

    let div = document.getElementById("myDiv");
    div.addEventListener("click", (event) => {
      let keys = new Array();
      if (event.shiftKey) {
        keys.push("shift");
      }
      if (event.ctrlKey) {
        keys.push("ctrl");
      }
      console.log("Keys: " + keys.join(","));
    });

    对 mousedown 和 mouseup 事件来说,event 对象上会有一个 button 属性,表示按下或释放的是哪个按键。0 表示鼠标主键、1 表示鼠标中键(通常是滚轮键)、2 表示鼠标副键。

    mousewheel 事件会在用户使用鼠标滚轮时触发,包括在垂直方向上任意滚动。这个事件会在任何元素上触发,并冒泡到 document 和 window。mousewheel 事件的 event 对象除了包含鼠标事件的所有标准信息之外,还有一个名为 wheelDelta 的新属性。当鼠标滚轮向前滚动时,wheelDelta 每次都是+120;而当鼠标滚轮向后滚动时,wheelDelta 每次都是–120。

    document.addEventListener("mousewheel", (event) => {
      console.log(event.wheelDelta);
    });

    键盘与输入事件

    键盘事件是用户操作键盘时触发的,其包含 3 个事件:

    • keydown:用户按下键盘上某个键时触发,而且持续按住会重复触发。
    • keyup:用户释放键盘上某个键时触发。
    • keypress:用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。DOM3 已废弃,推荐用 textInput 事件。

    输入事件只有一个,即 textInput。这个事件是对 keypress 事件的扩展,用于在文本显示给用户之前更方便地截获文本输入。textInput 会在文本被插入到文本框之前触发。当用户按下键盘上的某个字符键时,首先会触发 keydown 事件,然后触发 keypress 事件,最后触发 keyup 事件。

    对于 keydown 和 keyup 事件,event 对象的 keyCode 属性中会保存一个键码,对应键盘上特定的一个键。

    let textbox = document.getElementById("myText");
    textbox.addEventListener("keyup", (event) => {
      console.log(event.keyCode);
    });

    浏览器在 event 对象上支持 charCode 属性,只有发生 keypress 事件时这个属性才会被设置值,包含的是按键字符对应的 ASCII 编码。但是 DOM3 Events 规范并未规定 charCode 属性,而是定义了 key 和 char 两个新属性。key 属性用于替代 keyCode,且包含字符串。在按下字符键时,key 的值等于文本字符(如 “k”或“M”);在按下非字符键时,key 的值是键名(如“Shift”或“ArrowDown”)。char 属性在按下字符键时与 key 类似,在按下非字符键时为 null。

    DOM3 Events 规范增加了一个名为 textInput 的事件,其在字符被输入到可编辑区域时触发。textInput 事件主要关注字符,所以在 event 对象上提供了一个 data 属性,包含要插入的字符。data 的值始终是要被插入的字符,因此如果在按 S 键时没有按 Shift 键,data 的值就是"s",但在按 S 键时同时按 Shift 键,data 的值则是"S"。

    let textbox = document.getElementById("myText");
    textbox.addEventListener("textInput", (event) => {
      console.log(event.data);
    });

    HTML 事件

    contextmenu 事件专门用于表示何时应该显示上下文菜单,从而允许开发者取消默认的上下文菜单并提供自定义菜单。通常自定义的上下文菜单都是通过 oncontextmenu 事件处理程序触发显示,并通过 onclick 事件处理程序 触发隐藏的,例如:

    <!DOCTYPE html>
    <html>
      <head>
        <title>ContextMenu Event Example</title>
      </head>
      <body>
        <div id="myDiv">Right click or Ctrl+click me to get a custom context menu. Click anywhere else to get the default context menu.</div>
        <ul id="myMenu" style="position:absolute;visibility:hidden;background-color: silver">
          <li><a href="http://www.somewhere.com"> somewhere</a></li>
          <li><a href="http://www.wrox.com">Wrox site</a></li>
          <li><a href="http://www.somewhere-else.com">somewhere-else</a></li>
        </ul>
      </body>
    </html>

    上述例子中的

    元素有一个上下文菜单
      。作为上下文菜单,
        元素初始时是隐藏的。 通过以下 JavaScript 代码进行触发:

        window.addEventListener("load", (event) => {
          let div = document.getElementById("myDiv");
          div.addEventListener("contextmenu", (event) => {
            event.preventDefault();
            let menu = document.getElementById("myMenu");
            menu.style.left = event.clientX + "px";
            menu.style.top = event.clientY + "px";
            menu.style.visibility = "visible";
          });
          document.addEventListener("click", (event) => { 
            document.getElementById("myMenu").style.visibility = "hidden";
          }); 
        });

        这里在

        元素上指定了一个 oncontextmenu 事件处理程序。这个事件处理程序首先取消默认行,确保不会显示浏览器默认的上下文菜单。接着基于 event 对象的 clientX 和 clientY 属性把
          元素放到适当位置。最后一步通过将 visibility 属性设置为"visible"让自定义上下文菜单显示出来。另外,又给 document 添加了一个 onclick 事件处理程序,以便在单击事件发生时隐藏上下文菜单。

          beforeunload 事件会在 window 上触发,给开发者提供阻止页面被卸载的机会。这个事件会在页面即将从浏览器中卸载时触发,如果页面需要继续使用,则可以不被卸载。

          DOMContentLoaded 事件会在 DOM 树构建完成后立即触发,而不用等待图片、JavaScript 文件、CSS 文件或其他资源加载完成。相对于 load 事件在页面完全加载后触发,该事件可以让用户能够更快地与页面交互。

          readystatechange 事件提供文档或元素加载状态的信息。支持 readystatechange 事件的对象都有一个 readyState 属性,该属性具有一个字符串表示对象的加载状态:

          • uninitialized:对象存在并尚未初始化
          • loading:对象正在加载数据
          • loaded:对象已经加载完数据
          • interactive:对象可以交互,但尚未加载完成
          • complete:对象加载完成

          可以像下面这样使用 readystatechange 事件:

          document.addEventListener("readystatechange", (event) => { 
            if (document.readyState == "interactive") {
              console.log("Content loaded");
            }
          });

          hashchange 事件用于在 URL 散列值发生变化时通知开发者。onhashchange 事件处理程序必须添加给 window,event 对象有两个新属性:oldURL 和 newURL。这两个属性分别保存变化前后的 URL。

          window.addEventListener("hashchange", (event) => {
            console.log(`Old URL: ${event.oldURL}, New URL: ${event.newURL}`);
          });
          // 如果想确定当前的散列值,最好使用 location 对象:
          window.addEventListener("hashchange", (event) => {
            console.log(`Current hash: ${location.hash}`);
          });